﻿#define ICONS
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using SharpCmd.IO;
using SharpCmd.Modules;
using System.Text.RegularExpressions;

namespace SharpCmd.Windows.Forms
{
    internal partial class ListViewEx : ListView
    {
        #region fields
        private const int COLUMN_NAME = 0;
        private const int COLUMN_EXT = 1;
        private const int COLUMN_SIZE = 2;
        private const int COLUMN_DATE = 3;
        private const int COLUMN_ATTRIBUTES = 4;

        private static Color SelectionColor = Color.Red;

        private string quickSearch = "";
        private int systemTicksAtLastQuickSearch = 0;
        private RightMouseButtonDownInfo rmbInfo = null;
        private ListViewItem focusedItem;
        #endregion fields

        #region ctor
        public ListViewEx()
        {
            this.SuspendLayout();
            this.ListViewItemSorter = new ColumnSorter();
            this.ColumnClick += new ColumnClickEventHandler(this.ListView_ColumnClick);
            this.MouseMove += new MouseEventHandler(ListViewEx_MouseMove);
            this.DoubleBuffered = true;
            this.BorderStyle = System.Windows.Forms.BorderStyle.None;
            this.CausesValidation = false;
            this.Dock = System.Windows.Forms.DockStyle.Fill;
            this.Font = new System.Drawing.Font("Tahoma", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, (byte)238);
            this.FullRowSelect = true;
            this.HideSelection = false;
            this.OwnerDraw = true;
            this.ShowGroups = false;
            this.Sorting = System.Windows.Forms.SortOrder.Ascending;
            this.View = System.Windows.Forms.View.Details;
            this.DrawColumnHeader += new System.Windows.Forms.DrawListViewColumnHeaderEventHandler(this.ListViewEx_DrawColumnHeader);
            this.DrawItem += new System.Windows.Forms.DrawListViewItemEventHandler(this.ListViewEx_DrawItem);
            this.DrawSubItem += new System.Windows.Forms.DrawListViewSubItemEventHandler(this.ListViewEx_DrawSubItem);
            this.KeyDown += new System.Windows.Forms.KeyEventHandler(this.ListViewEx_KeyDown);
            this.KeyPress += new KeyPressEventHandler(ListViewEx_KeyPress);
            this.ResumeLayout();
        }
        #endregion ctor

        #region events
        public event EventHandler GoToParentDirectory;
        public event EventHandler FocusCmdBox;
        public event EventHandler EnterDirectoryUnderCursor;
        #endregion

        #region properties
        public new ListViewItem FocusedItem
        {
            get
            {
                return focusedItem;
            }
            set
            {
                focusedItem = value;
                focusedItem.EnsureVisible();
                focusedItem.Focused = true;
            }
        }

        public int ColumnIndexToSort
        {
            get
            {
                ColumnSorter sorter = this.ListViewItemSorter as ColumnSorter;
                return sorter.ColumnToSort;
            }
        }

        public SortOrder SortOrder
        {
            get
            {
                ColumnSorter sorter = this.ListViewItemSorter as ColumnSorter;
                return sorter.SortOrder;
            }
        }

        public long TotalSize
        {
            get
            {
                long size = 0;
                foreach (ListViewItemEx item in Items)
                    size += item.Info.Size;
                return size;
            }
        }

        public long SelectedSize
        {
            get
            {
                long selectedSize = 0;
                foreach (ListViewItemEx item in Items)
                    if (item.Selected)
                        selectedSize += item.Info.Size;
                return selectedSize;
            }
        }

        public int DirectoryCount
        {
            get
            {
                int count = 0;
                foreach (ListViewItemEx item in Items)
                    if (item.Info.Type == FSObjectType.Directory && !item.Info.Fake)
                        count++;
                return count;
            }
        }

        public int FileCount
        {
            get
            {
                int count = 0;
                foreach (ListViewItemEx item in Items)
                    if (item.Info.Type == FSObjectType.File && !item.Info.Fake)
                        count++;
                return count;
            }
        }

        public int SelectedFileCount
        {
            get
            {
                int count = 0;
                foreach (ListViewItemEx item in SelectedItems)
                    if (item.Info.Type == FSObjectType.File && !item.Info.Fake)
                        count++;
                return count;
            }
        }

        public int SelectedDirectoryCount
        {
            get
            {
                int count = 0;
                foreach (ListViewItemEx item in SelectedItems)
                    if (item.Info.Type == FSObjectType.Directory && !item.Info.Fake)
                        count++;
                return count;
            }
        }

        /// <summary>
        /// Gets selected items or focused item
        /// </summary>
        public List<FSObjectInfo> SelectedItemInfos
        {
            get
            {
                List<FSObjectInfo> selectedItems = new List<FSObjectInfo>();
                if (SelectedItems.Count == 0)
                    selectedItems.Add((FocusedItem as ListViewItemEx).Info);
                else
                    foreach (ListViewItemEx item in SelectedItems)
                        selectedItems.Add(item.Info);
                return selectedItems;
            }
        }

        /// <summary>
        /// Gets amount of items in one page in this ListViewEx
        /// </summary>
        private int PageSize
        {
            get
            {
                if (this.Items.Count == 0)
                    return 1;
                return (this.ClientSize.Height / this.Items[0].Bounds.Height) - 2;
            }
        }
        #endregion

        #region events
        private void ListViewEx_MouseMove(object sender, MouseEventArgs e)
        {
            if (rmbInfo == null)
                return;
            var item = GetItemAt(e.X, e.Y);
            if (item == null)
                return;
            //switch on status
            if (item == rmbInfo.FirstItem)
                return;
            item.Selected = rmbInfo.SelectionStatusToSet;
            this.FocusedItem = item;
        }

        protected override void WndProc(ref Message m)
        {
            switch (m.Msg)
            {
                case Windows.Events.WM_LBUTTONDOWN: //focus item with left click
                    this.Focus();
                    Point pos = this.PointToClient(Cursor.Position);
                    var item = GetItemAt(pos.X, pos.Y);
                    this.Focus();
                    if (item == null) //don't focus if we didn't click on an item
                        break;
                    this.FocusedItem = item;
                    break;
                case Windows.Events.WM_RBUTTONDOWN: //select item with right click
                    this.Focus();
                    pos = this.PointToClient(Cursor.Position);
                    item = GetItemAt(pos.X, pos.Y);
                    if (item == null)
                        break;
                    rmbInfo = new RightMouseButtonDownInfo();
                    rmbInfo.ClickTime = Environment.TickCount;
                    rmbInfo.FirstItem = item as ListViewItemEx;
                    //first we switch selection status of the item and set it as the status to get on mouse over
                    item.Selected = !item.Selected;
                    this.FocusedItem = item;
                    rmbInfo.SelectionStatusToSet = item.Selected;
                    break;
                case Windows.Events.WM_RBUTTONUP:
                    const int DELAY = 500;
                    if (Environment.TickCount - rmbInfo.ClickTime >= DELAY)
                    {
                        pos = this.PointToClient(Cursor.Position);
                        item = GetItemAt(pos.X, pos.Y);
                        if (item != null && item == rmbInfo.FirstItem)
                        {
                            //revert item's status
                            item.Selected = !item.Selected;
                            ShowContextMenu();
                        }
                    }
                    rmbInfo = null;
                    break;
                default:
                    base.WndProc(ref m);
                    break;
            }
        }

        #region key events
        private bool HandleSpecialKey(KeyEventArgs e)
        {
            //at all - we don't do nothing on unfocused item
            if (this.Items.Count == 0 || this.FocusedItem == null)
                return true;

            if (e.Control && e.Shift)
            {
                switch (e.KeyCode)
                {
                    case Keys.Up:
                        //open new background tab in the other DirectoryViewPanel
                        MessageBox.Show("open new background tab in the other DirectoryViewPanel");
                        return true;
                }
            }
            if (e.Control)
            {
                switch (e.KeyCode)
                {
                    case Keys.Left: //show right pane contents in left pane
                        MainForm.Instance.ShowRightPaneInLeft();
                        return true;
                    case Keys.Right: //show left pane contents in right pane
                        MainForm.Instance.ShowLeftPaneInRight();
                        return true;
                    case Keys.Up: //open new tab in background with selected directory!!
                        MessageBox.Show("CtrlUp");
                        MainForm.Instance.ActiveDirectoryViewPanel.OpenNewTab(false);
                        return true;
                    case Keys.Down:
                        if (FocusCmdBox != null)
                            FocusCmdBox(this, null);
                        //TODO show last command in CmdBox
                        return true;
                    case Keys.PageUp:
                        if (GoToParentDirectory != null)
                            GoToParentDirectory(this, null);
                        return true;
                    case Keys.PageDown:
                        MessageBox.Show("CtrlPgDn");
                        if (EnterDirectoryUnderCursor != null)
                            EnterDirectoryUnderCursor(this, null);
                        return true;
                    case Keys.Home:
                        this.FocusedItem = this.Items[0];
                        return true;
                    case Keys.End:
                        this.FocusedItem = this.Items[this.Items.Count - 1];
                        return true;
                    case Keys.A:
                        SelectAll();
                        return true;
                    case Keys.Enter:
                        MessageBox.Show("Ctrl+Enter"); //todo does not work
                        return true;
                    default:
                        break;
                }
            }
            else if (e.Shift)
            {
                switch (e.KeyCode)
                {
                    case Keys.PageUp:  //select to page up
                        int topItemIndex = Math.Max(0, this.FocusedItem.Index - PageSize);
                        HandleSelection(topItemIndex, this.FocusedItem.Index, true);
                        return true;
                    case Keys.PageDown:  //select to page down
                        int bottomItemIndex = Math.Min(this.Items.Count - 1, this.FocusedItem.Index + PageSize);
                        HandleSelection(this.FocusedItem.Index, bottomItemIndex, false);
                        return true;
                    case Keys.Home: //select to home
                        HandleSelection(0, this.FocusedItem.Index, true);
                        return true;
                    case Keys.End: //select to end
                        HandleSelection(this.FocusedItem.Index, this.Items.Count - 1, false);
                        return true;
                    case Keys.Up: //select up
                        //select item under focus and move focus up
                        this.FocusedItem.Selected = !this.FocusedItem.Selected;
                        MoveFocusUp();
                        return true;
                    case Keys.Down: //select down
                        this.FocusedItem.Selected = !this.FocusedItem.Selected;
                        MoveFocusDown();
                        return true;
                    case Keys.F10:
                        ShowContextMenu();
                        return true;
                }
            }
            switch (e.KeyCode)
            {
                case Keys.Up: //move focus up
                    MoveFocusUp();
                    return true;
                case Keys.Down://move focus down
                    MoveFocusDown();
                    return true;
                case Keys.PageUp:
                    this.FocusedItem = this.Items[Math.Max(0, this.FocusedItem.Index - PageSize)];
                    return true;
                case Keys.PageDown:
                    this.FocusedItem = this.Items[Math.Min(this.Items.Count - 1, this.FocusedItem.Index + PageSize)];
                    return true;
                case Keys.Home:
                    this.FocusedItem = this.Items[0];
                    return true;
                case Keys.End:
                    this.FocusedItem = this.Items[this.Items.Count - 1];
                    return true;
                case Keys.Left: //TODO move focus right in Brief and Thumbnails mode
                    if (this.View == System.Windows.Forms.View.Details)
                    {   //in details mode focus command window
                        if (FocusCmdBox != null)
                            FocusCmdBox(this, null);
                    }
                    return true;
                case Keys.Right: //TODO move focus right in Brief and Thumbnails mode
                    if (this.View == System.Windows.Forms.View.Details)
                    {   //in details mode focus command window
                        if (FocusCmdBox != null)
                            FocusCmdBox(this, null);
                    }
                    return true;
                case Keys.Back: //change directory UP
                    if (GoToParentDirectory != null)
                        GoToParentDirectory(this, null);
                    return true;
                case Keys.Insert: //invert selection on focused item and move down
                    this.FocusedItem.Selected = !this.FocusedItem.Selected;
                    MoveFocusDown();
                    return true;
                case Keys.Space:
                    if (!this.FocusedItem.Selected)
                    {
                        ListViewItemEx item = this.FocusedItem as ListViewItemEx;
                        if (item.Info.Type == FSObjectType.Directory)
                        {
                            long size = SharpCmd.Modules.FileSystem.FileSystemModule.Instance.GetDirectorySize(item.Info.FullName);
                            item.Info.SetSize(size);
                            item.RefreshSize();
                            this.Refresh();
                        }
                    }
                    this.FocusedItem.Selected = !this.FocusedItem.Selected;
                    return true;
                default:
                    break;
            }
            return false;
        }

        private void ShowContextMenu()
        {
            GongSolutions.Shell.ShellContextMenu cm = new GongSolutions.Shell.ShellContextMenu(new GongSolutions.Shell.ShellItem((OneSelectedItem as ListViewItemEx).Info.FullName));
            cm.ShowContextMenu(this, OneSelectedItem.Position);
        }

        private void ListViewEx_KeyDown(object sender, KeyEventArgs e)
        {
            bool handled = HandleSpecialKey(e);
            if (handled)
            {
                e.Handled = e.SuppressKeyPress = true;
                quickSearch = "";
            }
        }

        private void ListViewEx_KeyPress(object sender, KeyPressEventArgs e)
        {
            char ch = e.KeyChar;
            if (char.IsLetterOrDigit(ch))
            {   //do this ONLY if key is a letter/number
                ResetQuickSearchIfNeeded();
                quickSearch += ch;
                if (quickSearch != string.Empty)
                {
                    var item = this.FindItemWithText(quickSearch);
                    if (item != null)
                        this.FocusedItem = item;
                }
                e.Handled = true;
            }
            else
                quickSearch = "";
        }

        #endregion
        #endregion events

        //after MAX_DELAY milliseconds quicksearch resets
        private void ResetQuickSearchIfNeeded()
        {
            //TODO v.0.20-0.25 make MAX_DELAY configurable
            const int MAX_DELAY = 1000;
            int timeElapsed = Environment.TickCount - systemTicksAtLastQuickSearch;
            systemTicksAtLastQuickSearch = Environment.TickCount;
            if (timeElapsed > MAX_DELAY)
                quickSearch = "";
        }

        #region focus handling
        private void MoveFocusUp()
        {
            this.FocusedItem = this.Items[Math.Max(0, this.FocusedItem.Index - 1)];
        }

        private void MoveFocusDown()
        {
            this.FocusedItem = this.Items[Math.Min(this.Items.Count - 1, this.FocusedItem.Index + 1)];
        }
        #endregion

        #region sort
        public void ListView_ColumnClick(object sender, ColumnClickEventArgs e)
        {
            ListView senderListView = sender as ListView;
            ColumnSorter sorter = this.ListViewItemSorter as ColumnSorter;
            //already sorted column - reverse sorting order
            if (e.Column == sorter.ColumnToSort)
                sorter.SortOrder = sorter.SortOrder == SortOrder.Descending ? SortOrder.Ascending : SortOrder.Descending;
            else
            {
                sorter.ColumnToSort = e.Column;
                //sort SIZE and DATE columns descending by default
                sorter.SortOrder = (e.Column == COLUMN_SIZE || e.Column == COLUMN_DATE) ? SortOrder.Descending : System.Windows.Forms.SortOrder.Ascending;
            }
            this.Refresh(); //force repaint so other sorting arrows disappear
            this.Sort();
        }

        internal void Sort(SharpCmd.Controls.FileSystemViewers.SortType sortType)
        {
            ColumnSorter sorter = this.ListViewItemSorter as ColumnSorter;
            switch (sortType)
            {
                //if the column is already being sorted by, reverse the order
                case SharpCmd.Controls.FileSystemViewers.SortType.Name:
                    //sort order ? default : inverted
                    sorter.SortOrder = (sorter.ColumnToSort != COLUMN_NAME) ? SortOrder.Ascending : (sorter.SortOrder == SortOrder.Descending ? SortOrder.Ascending : SortOrder.Descending);
                    sorter.ColumnToSort = COLUMN_NAME;
                    break;
                case SharpCmd.Controls.FileSystemViewers.SortType.Extension:
                    sorter.SortOrder = (sorter.ColumnToSort != COLUMN_EXT) ? SortOrder.Ascending : (sorter.SortOrder == SortOrder.Descending ? SortOrder.Ascending : SortOrder.Descending);
                    sorter.ColumnToSort = COLUMN_EXT;
                    break;
                case SharpCmd.Controls.FileSystemViewers.SortType.Size:
                    sorter.SortOrder = (sorter.ColumnToSort != COLUMN_SIZE) ? SortOrder.Descending : (sorter.SortOrder == SortOrder.Descending ? SortOrder.Ascending : SortOrder.Descending);
                    sorter.ColumnToSort = COLUMN_SIZE;
                    break;
                case SharpCmd.Controls.FileSystemViewers.SortType.Date:
                    sorter.SortOrder = (sorter.ColumnToSort != COLUMN_DATE) ? SortOrder.Descending : (sorter.SortOrder == SortOrder.Descending ? SortOrder.Ascending : SortOrder.Descending);
                    sorter.ColumnToSort = COLUMN_DATE;
                    break;
            }
            this.Refresh(); //force repaint so other sorting arrows disappear
            this.Sort();
        }
        #endregion

        #region selection and focus
        /// <summary>
        /// Handles selection on shift+pageup/pagedown/home/end
        /// </summary>
        /// <param name="from"></param>
        /// <param name="to"></param>
        /// <param name="focusFirst">If true, new focused item is the first one (items[from]). If false, it's items[to]</param>
        private void HandleSelection(int from, int to, bool focusFirst)
        {
            //set or unset selection based on focused item index
            bool newState = !this.FocusedItem.Selected;
            for (int i = from; i <= to; i++)
                this.Items[i].Selected = newState;
            this.FocusedItem = this.Items[focusFirst ? from : to];
            this.FocusedItem.EnsureVisible();
        }

        /// <summary>
        /// Inverts selection in this ListViewEx
        /// </summary>
        public void InvertSelection()
        {
            foreach (ListViewItem item in this.Items)
                item.Selected = !item.Selected;
        }

        /// <summary>
        /// Selects all items in the ListViewEx
        /// </summary>
        public void SelectAll()
        {
            foreach (ListViewItemEx item in this.Items)
                if (!item.Info.Fake)
                    item.Selected = true;
        }

        /// <summary>
        /// Get first selected item or the item under focus
        /// </summary>
        public ListViewItem OneSelectedItem
        {
            get
            {
                if (SelectedItems.Count > 0)
                    return SelectedItems[0];
                else
                    return FocusedItem;
            }
        }

        /// <summary>
        /// Focuses first item in ListViewEx
        /// </summary>
        public void FocusItem()
        {
            if (Items.Count == 0)
                return;
            this.FocusedItem = Items[0];
        }
        #endregion

        #region owner draw
        private void ListViewEx_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e)
        {
            e.DrawBackground();
            e.DrawText();
            if (e.ColumnIndex == this.ColumnIndexToSort)
            {
                //draw arrow only in 'sorted' columns
                var headerTextSize = e.Graphics.MeasureString(e.Header.Text, e.Font);
                int location;
                if (e.Header.TextAlign == HorizontalAlignment.Left)
                    location = e.Bounds.Left + (int)(headerTextSize.Width + 10);
                else
                    location = e.Bounds.Right - 22 - (int)headerTextSize.Width;
                DrawArrow(e.Graphics, location, this.SortOrder == SortOrder.Ascending);
            }
        }

        private void DrawArrow(Graphics g, int offsetX, bool arrowFacingUp)
        {
            const int WIDTH = 4;
            const int HEIGHT = 16;
            g.DrawLine(Pens.Black, offsetX + WIDTH, 1, offsetX + WIDTH, HEIGHT - 1);
            if (arrowFacingUp)
            {
                g.DrawLine(Pens.Black, offsetX, 5, offsetX + WIDTH, 1);
                g.DrawLine(Pens.Black, offsetX + (WIDTH * 2), 5, offsetX + WIDTH, 1);
            }
            else
            {
                g.DrawLine(Pens.Black, offsetX, 11, offsetX + WIDTH, HEIGHT - 1);
                g.DrawLine(Pens.Black, offsetX + (WIDTH * 2), 11, offsetX + WIDTH, HEIGHT - 1);
            }
        }

        private void ListViewEx_DrawItem(object sender, DrawListViewItemEventArgs e)
        {
            e.DrawDefault = true;
        }

        private void ListViewEx_DrawSubItem(object sender, DrawListViewSubItemEventArgs e)
        {
            Brush brush;
            if (e.Item.Selected)
                brush = new SolidBrush(ListViewEx.SelectionColor);
            else
                brush = new SolidBrush(e.SubItem.ForeColor);
            //Name+icon column
            if (e.ColumnIndex == 0)
            {
                ListViewItemEx item = e.Item as ListViewItemEx;
                const int IconWidth = 16;
#if ICONS
                Icon icon = null;
                if (item.Info.Type == FSObjectType.Directory)
                    icon = IconManager.GetDirectoryIcon(item.Info.FullName, IconManager.IconSize.Small, true);
                else
                    icon = IconManager.GetFileIcon(item.Info.FullName, IconManager.IconSize.Small, true);
                //TODO add option of drawing only default icons, not associated & overlay ones
                e.Graphics.DrawIcon(icon, e.Bounds.Left, e.Bounds.Top);
#endif
                //focus rectangle
                //TODO draw focus rectangle using entire control width, not only shown columns
                if (e.Item.Focused && this.Focused)
                    ControlPaint.DrawFocusRectangle(e.Graphics, e.Item.Bounds);
                //text
                e.Graphics.DrawString(item.Info.DisplayString, e.SubItem.Font, brush, e.Bounds.Left + IconWidth, e.Bounds.Top);
            }
            else
            {
                //set up string alignment
                StringFormat fmt = new StringFormat();
                switch (e.Header.TextAlign)
                {
                    case HorizontalAlignment.Right:
                        fmt.Alignment = StringAlignment.Far;
                        e.Graphics.DrawString(
                            e.SubItem.Text,
                            e.SubItem.Font,
                            brush,
                            new RectangleF(0, e.Bounds.Top, e.Bounds.Right, e.Bounds.Bottom),
                            fmt);
                        break;
                    case HorizontalAlignment.Center:
                        fmt.Alignment = StringAlignment.Center;
                        break;
                    default:
                        fmt.Alignment = StringAlignment.Near;
                        e.Graphics.DrawString(
                            e.SubItem.Text,
                            e.SubItem.Font,
                            brush,
                            new RectangleF(e.Bounds.Left, e.Bounds.Top, this.Width, e.Bounds.Height),
                            fmt);
                        break;
                }
            }
        }
        #endregion owner draw

        internal void FocusItemWithPath(string oldPath)
        {
            foreach (ListViewItemEx item in Items)
            {
                if (item.Info.FullName == oldPath)
                {
                    this.FocusedItem = item;
                    return;
                }
            }
        }

        internal void SelectSameExtension()
        {
            if (FocusedItem == null)
                return;
            var item = FocusedItem as ListViewItemEx;
            ExpandGroupByExtension(item.Info.Extension);
        }

        private void ExpandGroupByExtension(string extension)
        {
            foreach (ListViewItemEx item in Items)
                if (item.Info.Extension == extension)
                    item.Selected = true;
        }

        public void SetSelectedForFilter(string filter, bool selected)
        {
            //replace ? with . and * with .*
            string escapedFilter = Regex.Escape(filter).Replace("\\?", ".").Replace("\\*", ".*");
            escapedFilter = "^" + escapedFilter + "$";
            Regex r = new Regex(escapedFilter, RegexOptions.IgnoreCase);
            foreach (ListViewItemEx item in Items)
            {
                if (item.Info.Fake)
                    continue;
                if (r.IsMatch(item.Info.NameWithExtension))
                    item.Selected = selected;
            }
        }
    }
}